#!/usr/bin/env python
# coding=utf-8


from collections import Counter
from typing import Literal, Self


class GameSolution(object):
    __slots__ = ("_guesses", "_solutions")

    def __init__(self, solutions: list[list[list[int]]]):
        self._guesses = []
        self._solutions: list[list] = []
        self.__set_solutions(solutions)

    def __len__(self):
        return len(self._solutions)

    def __set_solutions(self, solutions: list[list[list[int]]]):
        for _solutions in solutions:
            tmp = []
            for row in range(len(_solutions)):
                for col in range(len(_solutions[row])):
                    if _solutions[row][col] > 0:
                        tmp.append((row, col))
            if tmp != []:
                self._solutions.append(tmp)
        if len(self._solutions) == 0:
            raise ValueError("The puzzle has not been confirmed")

    def change(self, row: int, col: int, target: Literal[-1, 1]) -> Self:
        """change board stat

        Args:
            row (int): row of board
            col (int): col of board
            target (int): target of board, enum in {-1, 1}

        Returns:
            Self: chained calls
        """
        if len(self._solutions) == 0 or target == 0:
            return self
        if target > 0:
            self._guesses.append((row, col))
        for solution in self._solutions[::-1]:
            """ 
            TODO: tmp container to catch result
            """
            if target > 0 and (row, col) not in solution or target < 0 and (row, col) in solution:
                self._solutions.remove(solution)
        if len(self._solutions) == 0:
            return self
        solution = self._solutions[0]
        for _guess in self._guesses[::-1]:
            if _guess in solution:
                solution.remove(_guess)
        return self

    def guess(self) -> tuple[int, int] | None:
        if self._solutions is None or len(self._solutions) == 0 or len(solution := self._solutions[0]) == 0:
            return None
        return solution[-1]

    @property
    def guessed(self):
        return self._guesses


class GameParser(object):
    __slot__ = ()

    def __init__(self):
        self.board: list[list[int]] = None
        self._blocks: list[list[list[int]]] = []
        self.block_set_counter = Counter()
        self.target: int = 8

    @property
    def row_board(self):
        return len(self.board)

    @property
    def col_board(self):
        return len(self.board[0])

    def row_block(self, index: int):
        return len(self.blocks[index])

    def col_block(self, index: int):
        return len(self.blocks[index][0])

    def can_place(self, index: int, row: int, col: int) -> bool:
        for r in range(self.row_block(index)):
            for c in range(self.col_block(index)):
                # both zero or all not zero
                if self.board[row + r][col + c] == 0:
                    continue
                if (
                    self.board[row + r][col + c] < 0
                    and self.blocks[index][r][c] > 0
                    or self.board[row + r][col + c] > 0
                    and self.blocks[index][r][c] < 0
                ):
                    return False
        return True

    def _set_block(self, target: Literal[-1, 1], index: int, row: int, col: int):
        for r in range(self.row_block(index)):
            for c in range(self.col_block(index)):
                # both row and col are less than 9
                self.block_set_counter[10 * (row + r) + col + c] += self.blocks[index][r][c] * target
                self.board[row + r][col + c] += self.blocks[index][r][c] * target

    @property
    def blocks(self):
        return self._blocks

    @blocks.setter
    def blocks(self, val: list[list[int]]):
        self._blocks.extend(val)

    def _solve(self) -> GameSolution:
        solutions: list[list[list[int]]] = []

        def _backtrack(deepth: int = 0):
            if deepth == len(self.blocks):
                if sum(True for v in self.block_set_counter.values() if v > 0) == self.target:
                    solutions.append([row[:] for row in self.board])
                return
            for row in range(self.row_board - 2):
                for col in range(self.col_board - 2):
                    if self.can_place(deepth, row, col):
                        self._set_block(1, deepth, row, col)
                        _backtrack(deepth + 1)
                        self._set_block(-1, deepth, row, col)

        _backtrack(0)
        return GameSolution(solutions)

    def solve(
        self,
        board: list[list[int]] = None,
        blocks: list[list[list[int]]] = None,
        target: int = 0,
    ) -> GameSolution:
        """solve pazzle with given options

        If option is None, try using the previous option

        Args:
            board (list[list[int]], optional): game board. Defaults to None.
            blocks (list[list[list[int]]], optional): game blocks. Defaults to None.
            target (int, optional): game target. Defaults to 0.

        Raises:
            ValueError: options are None or solve failed

        Returns:
            GameSolution: result of solve
        """

        if (self.board is None and board is None) or (self.blocks is None and blocks is None):
            raise ValueError("empty set, check board and blocks")
        if board is not None:
            self.board = board
        if blocks is not None:

            def isempty(block: list[list[int]]) -> bool:
                row = len(block)
                col = len(block[0])
                for r in range(row):
                    for c in range(col):
                        if block[r][c] != -1:
                            return False
                return True

            for block in blocks[::-1]:
                if isempty(block):
                    blocks.remove(block)
            if len(blocks) == 0:
                raise ValueError("empty blocks, check your blocks")
            self.blocks = blocks
        if target != 0:
            self.target = target
        self.block_set_counter.clear()

        return self._solve()
